/**
 * (c) Copyright Ascensio System SIA 2025
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zzvcom.office.documentserver.storage;

import cn.hutool.core.io.FileUtil;
import com.zzvcom.office.cfg.AppConfig;
import com.zzvcom.office.entities.OfficeFile;
import com.zzvcom.office.entities.OfficePdf;
import com.zzvcom.office.repositories.OfficeFileRepository;
import com.zzvcom.office.services.OfficeFileServices;
import com.zzvcom.office.util.IdUtil;
import com.zzvcom.office.util.MyFileUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Component;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

// todo: Refactoring
@Component
@Primary
@Slf4j
public class LocalFileStorage implements FileStorageMutator, FileStoragePathBuilder {

    @Autowired
    private OfficeFileServices officeFileServices;
    @Autowired
    private AppConfig appConfig;
    @Getter
    private String storageAddress;

    @Value("${files.storage.folder}")
    private String storageFolder;


    @Value("${files.docservice.history.postfix}")
    private String historyPostfix;

    @Autowired
    private HttpServletRequest request;

    @Value("${filename-max}")
    private String filenameMax;
    @Autowired
    private MyFileUtil myFileUtil;

    /*
        This Storage configuration method should be called whenever a new storage folder is required
     */
    public void configure(final String address) {
        this.storageAddress = "localhost";
        if (this.storageAddress == null) {
            try {
                this.storageAddress = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                this.storageAddress = "unknown_storage";
            }
        }
        this.storageAddress = this.storageAddress.replaceAll("[^0-9a-zA-Z.=]", "_");
        createDirectory(Paths.get(getStorageLocation()));
    }

    public String getFileName(final String url) {
        return myFileUtil.getFileName(url);
    }

    /**
     * get the storage directory
     * 获取仓库的地址
     *
     * @return
     */
    public String getStorageLocation() {
        String directory;
        if (Paths.get(this.storageAddress).isAbsolute()) {
            directory = this.storageAddress + File.separator;
        } else {
            directory = storageFolder
                    + File.separator + this.storageAddress
                    + File.separator;
        }
        if (!Files.exists(Paths.get(directory))) {
            createDirectory(Paths.get(directory));
        }
        return directory;
    }

    /**
     * 获取文件所在的仓库路径，就是绝对路径
     *
     * @param fileName   文件名称
     * @param officeFile 所属文件试题
     * @return
     */
    public String getFileLocation(OfficeFile officeFile, final String fileName) {
        String path = officeFile.getPath();

        if (fileName.contains(File.separator)) {
            //  return getStorageLocation() + fileName;
            return Paths.get(getStorageLocation(), path, fileName).normalize().toString();
        }
        return Paths.get(getStorageLocation(), path, fileName).normalize().toString();
        // return getStorageLocation() + getFileName(fileName);
    }

    /**
     * 获取pdf目录路径
     *
     * @param file
     * @param fileName
     * @return
     */
    public String getFileLocationPdf(OfficePdf officePdf, String fileName) {
        String path = officePdf.getPath();
        if (fileName.contains(File.separator)) {
            //  return getStorageLocation() + fileName;
            return Paths.get(getStorageLocation(), path, fileName).normalize().toString();
        }
        return Paths.get(getStorageLocation(), path, fileName).normalize().toString();
    }

    /**
     * 如果不存，创建一个目录
     *
     * @param path
     */
    public void createDirectory(final Path path) {
        if (Files.exists(path)) {
            return;
        }
        try {
            Files.createDirectories(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 如果不存在，创建一个文件，并把流中的文件写入到path中。
     * 流要自动关闭
     *
     * @param path   写入的本地路径
     * @param stream 文件流
     * @return 如果文件已经存在，返回true,否则返回false
     */
    public boolean createFileNotExists(final Path path, final InputStream stream) {
        return myFileUtil.createFileNotExists(path, stream);
    }

    // delete a file
    public boolean deleteFile(OfficeFile officeFile, final String fileNameParam) {

        String fileName = URLDecoder
                .decode(fileNameParam, StandardCharsets.UTF_8);  // decode a x-www-form-urlencoded string
        if (fileName.isBlank()) {
            return false;
        }
        String filenameWithoutExt = getFileNameWithoutExtension(fileName);  // get file name without extension

        Path filePath = fileName.contains(File.separator)
                ? Paths.get(fileName) : Paths.get(getFileLocation(officeFile, fileName));  // get the path to the file
        Path filePathWithoutExt = fileName.contains(File.separator)
                ? Paths.get(filenameWithoutExt) : Paths
                .get(getStorageLocation() + filenameWithoutExt);  // get the path to the file without extension

        // delete the specified file; for directories, recursively delete any nested directories or files as well
        boolean fileDeleted = FileSystemUtils.deleteRecursively(filePath.toFile());
        /* delete the specified file without extension; for directories,
         recursively delete any nested directories or files as well */
        boolean fileWithoutExtDeleted = FileSystemUtils.deleteRecursively(filePathWithoutExt.toFile());

        return fileDeleted && fileWithoutExtDeleted;
    }

    // delete file history
    public boolean deleteFileHistory(final String fileNameParam) {
        String fileName = URLDecoder
                .decode(fileNameParam, StandardCharsets.UTF_8);  // decode a x-www-form-urlencoded string
        if (fileName.isBlank()) {
            return false;
        }

        Path fileHistoryPath = Paths
                .get(getStorageLocation() + getHistoryDir(fileName));  // get the path to the history file
        Path fileHistoryPathWithoutExt = Paths.get(getStorageLocation() + getHistoryDir(
                getFileNameWithoutExtension(fileName)));  // get the path to the history file without extension

        /* delete the specified history file; for directories,
         recursively delete any nested directories or files as well */
        boolean historyDeleted = FileSystemUtils.deleteRecursively(fileHistoryPath.toFile());

        /* delete the specified history file without extension; for directories,
         recursively delete any nested directories or files as well */
        boolean historyWithoutExtDeleted = FileSystemUtils.deleteRecursively(fileHistoryPathWithoutExt.toFile());

        return historyDeleted || historyWithoutExtDeleted;
    }

    // delete the user's folder recursively
    public boolean deleteUserFolder() {
        return FileSystemUtils.deleteRecursively(new File(getStorageLocation()));
    }

    // update a file
    public OfficeFile updateFile(MultipartFile file) {
        String fileName = file.getOriginalFilename();
        OfficeFile officeFile = new OfficeFile();
        officeFile.newUpload(IdUtil.getId());
        officeFile.setExt(FileUtil.getSuffix(fileName));
        // 重命名后的老师
        officeFile.setName(officeFile.getId() + "." + officeFile.getExt());
        //原始的文件名称
        officeFile.setSourceName(fileName);
        officeFile.setTitle(fileName);
        String fileLocation = getFileLocation(officeFile, officeFile.getName());
        try {
            //进行文件的写入，并计算大小，信息写入数据库中。
            Path path = Paths.get(fileLocation);
            createFileNotExists(path, file.getInputStream());
            officeFile.setSize(Files.size(path));
            officeFileServices.save(officeFile);
            return officeFile;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    // move a file to the specified destination
    public boolean moveFile(final Path source, final Path destination) {
        try {
            Files.move(source, destination,
                    new StandardCopyOption[]{StandardCopyOption.REPLACE_EXISTING});
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    // write the payload to the file
    public boolean writeToFile(final String pathName, final String payload) {
        try (FileWriter fw = new FileWriter(pathName)) {
            fw.write(payload);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 该方法用于获取或创建强制保存文件版本的存储路径。
     * 首先检查存储目录是否存在，
     * 若不存在则返回空字符串；然后构建历史文件目录路径，
     * 若不需创建且目录不存在也返回空；接着创建该目录；最后拼接完整文件路径并返回。
     *
     * @param fileName
     * @param create
     * @return
     */
    // get the path where all the forcely saved file versions are saved or create it
    public String getForcesavePath(OfficeFile officeFile, final String fileName, final Boolean create) {

        String directory = getStorageLocation();
        Path path = Paths.get(directory);  // get the storage directory
        if (!Files.exists(path)) {
            return "";
        }
        directory = getFileLocation(officeFile, fileName) + historyPostfix + File.separator;
        path = Paths.get(directory);   // get the history file directory
        if (!create && !Files.exists(path)) {
            return "";
        }

        createDirectory(path);  // create a new directory where all the forcely saved file versions will be saved

        directory = directory + fileName;
        path = Paths.get(directory);
        if (!create && !Files.exists(path)) {
            return "";
        }

        return directory;
    }

    /**
     * load file as a resource
     * 该方法用于根据文件名加载本地存储的文件资源：
     * 首先尝试获取强制保存路径下的文件位置；
     * 如果路径为空，则使用普通文件位置；
     * 将文件路径转换为URL资源；
     * 若资源存在则返回，否则返回null。
     *
     * @param fileName
     * @return
     */
    public Resource loadFileAsResource(OfficeFile officeFile, final String fileName) {
        String fileLocation = getForcesavePath(officeFile, fileName,
                false);  // get the path where all the forcely saved file versions are saved
        if (fileLocation.isBlank()) {  // if file location is empty
            fileLocation = getFileLocation(officeFile, fileName);  // get it by the file name
        }
        try {
            Path filePath = Paths.get(fileLocation);  // get the path to the file location
            Resource resource = new UrlResource(filePath.toUri());  // convert the file path to URL
            if (resource.exists()) {
                return resource;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Resource loadFileAsResourcePdf(OfficePdf officeFile, String fileName) {
        String fileLocationPdf = getFileLocationPdf(officeFile, fileName);
        try {
            Path filePath = Paths.get(fileLocationPdf);  // get the path to the file location
            Resource resource = new UrlResource(filePath.toUri());  // convert the file path to URL
            if (resource.exists()) {
                return resource;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return null;

    }

    public Resource loadFileAsResourceHistory(final String fileName, final String version, final String file) {

        String fileLocation = getStorageLocation() + fileName + "-hist" + File.separator + version
                + File.separator + file;  // get it by the file name
        log.info(fileLocation);
        try {
            Path filePath = Paths.get(fileLocation);  // get the path to the file location
            Resource resource = new UrlResource(filePath.toUri());  // convert the file path to URL
            if (resource.exists()) {
                return resource;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return null;
    }

    // get a collection of all the stored files
    public File[] getStoredFiles() {
        File file = new File(getStorageLocation());
        File[] files = file.listFiles(pathname -> pathname.isFile());
        Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed());
        return files;
    }

    @SneakyThrows
    public void createMeta(
            OfficeFile officeFile,
            final String fileName,
            final String uid,
            final String uname) {  // create the file meta information
        String histDir = getHistoryDir(getFileLocation(officeFile, fileName));  // get the history directory
        Path path = Paths.get(histDir);  // get the path to the history directory
        createDirectory(path);  // create the history directory
        // create the json object with the file metadata
        JSONObject json = new JSONObject();
        json.put("created", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                .format(new Date()));  // put the file creation date to the json object
        json.put("id", uid);  // put the user ID to the json object
        json.put("name", uname);  // put the user name to the json object

        File meta = new File(histDir + File.separator
                + "createdInfo.json");  // create the createdInfo.json file with the file meta information
        try (FileWriter writer = new FileWriter(meta)) {
            json.writeJSONString(writer);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        log.debug("meta={},json={}", meta, json.toJSONString());
    }

    // create or update a file
    public boolean createOrUpdateFile(final Path path, final InputStream stream) {
        if (!Files.exists(path)) { // if the specified file does not exist
            return createFileNotExists(path, stream);  // create it in the specified directory
        } else {
            try {
                //  Files.write(path, stream
                //        .readAllBytes());  // otherwise, write new information in the bytes format to the file
                FileUtil.writeFromStream(stream, path.toFile());
                return true;
            } catch (Exception e) {
                log.error("createOrUpdateFile error: {}", e);
            }
        }
        return false;
    }

    // get the server URL
    public String getServerUrl(final Boolean forDocumentServer) {
//        if (forDocumentServer && !docserviceUrlExample.equals("")) {
//            return docserviceUrlExample;
//        } else {
//            // 不使用
//            return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
//                    + request.getContextPath();
//        }
        return appConfig.getMyAppServiceUrl();
    }

    // get the history directory
    public String getHistoryDir(final String path) {
        return path + historyPostfix;
    }

    /**
     * 获取文件历史目录
     *
     * @param officeFile
     * @param fileName
     * @return
     */
    public String getFileLocationHistoryDir(OfficeFile officeFile, final String fileName) {
        return getHistoryDir(getFileLocation(officeFile, fileName));
    }

    // get the file version

    /**
     * 返回文件的版本，如果目录下没有一个文件则当前文件版本为1，否则就是目录下直接子目录数量+1(自身目录)
     *
     * @param historyPath
     * @param ifIndexPage
     * @return
     */
    public int getFileVersion(final String historyPath, final Boolean ifIndexPage) {
        Path path;
        if (ifIndexPage) {  // if the start page is opened
            path = Paths.get(getStorageLocation()
                    + getHistoryDir(historyPath));  // get the storage directory and add the history directory to it
        } else {
            // 如果路径不存在则版本为1
            path = Paths.get(historyPath);  // otherwise, get the path to the history directory
            if (!Files.exists(path)) {
                return 1;  // if the history directory does not exist, then the file version is 1
            }
        }
        /**
         * 仅仅返回下面直接目录的数量【包含自身】 数量
         */
        // run through all the files in the history directory
        try (Stream<Path> stream = Files.walk(path, 1)) {
            Set<String> collect = stream
                    .filter(file -> Files.isDirectory(file))  // take only directories from the history folder
                    .map(Path::getFileName)  // get file names
                    .map(Path::toString)  // and convert them into strings
                    .collect(Collectors.toSet());
            log.info("collect={}", collect);
            return collect.size();  /* convert stream into set
                     and get its size which specifies the file version */
        } catch (IOException e) {
            log.error("getFileVersion=", e);
            return 0;
        }
    }

    public Path generateFilepath(final String directory, final String fullFileName) {
        int maxName = Integer.parseInt(filenameMax);
        String fileName = getFileNameWithoutExtension(fullFileName);
        if (fileName.length() > maxName) {
            fileName = fileName.substring(0, maxName) + "[...]";
        }
        String fileExtension = getFileExtension(fullFileName);  // get file extension
        // get the path to the files with the specified name
        Path path = Paths.get(directory + fileName + "." + fileExtension);

        for (int i = 1; Files.exists(path); i++) {  // run through all the files with the specified name
            // get a name of each file without extension and add an index to it
            fileName = fileName + "(" + i + ")";
            // create a new path for this file with the correct name and extension
            path = Paths.get(directory + fileName + "." + fileExtension);
        }

        path = Paths.get(directory + fileName + "." + fileExtension);
        return path;
    }

    private String getFileNameWithoutExtension(final String url) {
        String fileName = getFileName(url);
        if (fileName == null) {
            return null;
        }
        String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
        return fileNameWithoutExt;
    }

    private String getFileExtension(final String url) {
        String fileName = getFileName(url);
        if (fileName == null) {
            return null;
        }
        String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
        return fileExt.toLowerCase();
    }
}
